WebSocket 协议详解 您所在的位置:网站首页 websocket 协议 WebSocket 协议详解

WebSocket 协议详解

2023-04-30 08:48| 来源: 网络整理| 查看: 265

一、WebSocket 协议背景

早期,在网站上推送消息给用户,只能通过轮询的方式或 Comet 技术。轮询就是浏览器每隔几秒钟向服务端发送 HTTP 请求,然后服务端返回消息给客户端。

轮询技术一般在浏览器上就是使用 setInerval 或 setTimeout

这种方式的缺点:

需要不断的向服务端发送 HTTP 请求,这种就比较浪费带宽资源。而且发送 HTTP 请求只能由客户端发起,这也是早期 HTTP1.0/1.1 协议的一个缺点。它做不到由服务端向客户端发起请求。

为了能实现客户端和服务端的双向通信,经过多年发展于是 WebSocket 协议在 2008 年就诞生了。

它最初是在 HTML5 中引入的。经过多年发展后,该协议慢慢被多个浏览器支持,RFC 在 2011 年就把该协议作为一个国际标准,叫 rfc6455。

二、协议简介

WebSocket 是一种支持双向通信的网络协议。

双向通信:客户端(比如浏览器)可以向服务端发送消息,服务端也可以主动向客户端发送消息。

这样就实现了客户端和服务端的双向通信,那么上面所说的消息推送就比较容易实现了。

原先的 HTTP1.0/1.1 只能是客户端向服务端发送消息。

协议特点:

建立在 TCP 协议之上。 WebSocket 协议是从 HTTP 协议升级而来。 与 HTTP 协议良好兼容新。默认端口是 80 和 443,握手阶段采用 HTTP 协议。 数据格式比较轻量,通信效率高,性能开销小。 可以发送文本,也可以发送二进制数据。 没有同源限制,客户端可以与任意服务端通信。 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。 可以支持扩展,定了扩展协议。 保持连接状态,websocket 是一种有状态的协议,通信就可以省略部分状态信息。 实时性更强,因为是双向通信协议,所以服务端可以随时向客户端发送数据。 三、HTTP 升级到 WebSocket 过程

WebSocket 协议建立复用了 HTTP 的握手请求过程。

客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议。协议完成后,后续的数据交互则遵循 WebSocket 的协议。

客户端发起协议升级请求 GET / HTTP/1.1 Host: localhost:8080 Origin: http://127.0.0.1:3000 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

说明:上面请求信息忽略了 HTTP 的一些非必要头部请求信息,剔除多余的干扰。

Origin: http://127.0.0.1:3000 : 原始的协议和URL Connection: Upgrade:表示要升级协议了 Upgrade: websocket:表示要升级到 WebSocket 协议; Sec-WebSocket-Version: 13:表示 WebSocket 的版本。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader ,里面包含服务端支持的版本号 Sec-WebSocket-Key:与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接 服务端响应协议升级 HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

HTTP/1.1 101 Switching Protocols: 状态码 101 表示协议切换

Sec-WebSocket-Accept:根据客户端请求首部的 Sec-WebSocket-Key 计算出来

将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

通过 SHA1 计算出摘要,并转成 base64 字符串。计算公式如下:

Base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))

Connection:Upgrade:表示协议升级

Upgrade: websocket:升级到 websocket 协议

四、WebSocket 数据交换 数据帧格式

在 WebSocket 协议中,客户端与服务端数据交换的最小信息单位叫做帧(frame),由 1 个或多个帧按照次序组成一条完整的消息(message)。

数据传输的格式是由 ABNF 来描述的。

WebSocket 数据帧的统一格式如下图:

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+

(https://www.rfc-editor.org/rfc/rfc6455.html#section-5.2 Base Framing Protocol)

上面图中名词解释:

名词 说明 大小 FIN 如果是 1,表示这是消息(message)的最后一个分片(fragment);如果是 0,表示不是是消息(message)的最后一个分片(fragment) 1 个比特 RSV1, RSV2, RSV3 一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非 0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错 各占 1 个比特 opcode 操作代码,Opcode 的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection) 4 个比特 mask 表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。如果 Mask 是 1,那么在 Masking-key 中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask 都是 1。 1 个比特 Payload length 数据载荷的长度,单位是字节。假设数 Payload length === x,如果:x 为 0~126:数据的长度为 x 字节。 x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度。 x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。此外,如果 payload length 占用了多个字节的话,payload length 的二进制表达采用网络序(big endian,重要的位在前)。 为 7 位,或 7+16 位,或 1+64 位。 Masking-key 所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask 为 1,且携带了 4 字节的 Masking-key。如果 Mask 为 0,则没有 Masking-key。备注:载荷数据的长度,不包括 mask key 的长度。 0 或 4 字节(32 位 Payload data 载荷数据:包括了扩展数据、应用数据。其中,扩展数据 x 字节,应用数据 y 字节。The "Payload data" is defined as "Extension data" concatenated with "Application data".扩展数据:如果没有协商使用扩展的话,扩展数据数据为 0 字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。 应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。 (x+y) 字节

表中 opcode 操作码:

%x0:表示一个延续帧(continuation frame)。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。 %x1:表示这是一个文本帧(frame),text frame %x2:表示这是一个二进制帧(frame),binary frame %x3-7:保留的操作代码,用于后续定义的非控制帧。 %x8:表示连接断开。connection close %x9:表示这是一个 ping 操作。a ping %xA:表示这是一个 pong 操作。a pong %xB-F:保留的操作代码,用于后续定义的控制帧。 数据帧另外一种表达方式 ws-frame = frame-fin ; 1 bit in length frame-rsv1 ; 1 bit in length frame-rsv2 ; 1 bit in length frame-rsv3 ; 1 bit in length frame-opcode ; 4 bits in length frame-masked ; 1 bit in length frame-payload-length ; either 7, 7+16, ; or 7+64 bits in ; length [ frame-masking-key ] ; 32 bits in length frame-payload-data ; n*8 bits in ; length, where ; n >= 0 frame-fin = %x0 ; more frames of this message follow / %x1 ; final frame of this message ; 1 bit in length frame-rsv1 = %x0 / %x1 ; 1 bit in length, MUST be 0 unless ; negotiated otherwise frame-rsv2 = %x0 / %x1 ; 1 bit in length, MUST be 0 unless ; negotiated otherwise frame-rsv3 = %x0 / %x1 ; 1 bit in length, MUST be 0 unless ; negotiated otherwise frame-opcode = frame-opcode-non-control / frame-opcode-control / frame-opcode-cont frame-opcode-cont = %x0 ; frame continuation frame-opcode-non-control= %x1 ; text frame / %x2 ; binary frame / %x3-7 ; 4 bits in length, ; reserved for further non-control frames frame-opcode-control = %x8 ; connection close / %x9 ; ping / %xA ; pong / %xB-F ; reserved for further control ; frames ; 4 bits in length frame-masked = %x0 ; frame is not masked, no frame-masking-key / %x1 ; frame is masked, frame-masking-key present ; 1 bit in length frame-payload-length = ( %x00-7D ) / ( %x7E frame-payload-length-16 ) / ( %x7F frame-payload-length-63 ) ; 7, 7+16, or 7+64 bits in length, ; respectively frame-payload-length-16 = %x0000-FFFF ; 16 bits in length frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF ; 64 bits in length frame-masking-key = 4( %x00-FF ) ; present only if frame-masked is 1 ; 32 bits in length frame-payload-data = (frame-masked-extension-data frame-masked-application-data) ; when frame-masked is 1 / (frame-unmasked-extension-data frame-unmasked-application-data) ; when frame-masked is 0 frame-masked-extension-data = *( %x00-FF ) ; reserved for future extensibility ; n*8 bits in length, where n >= 0 frame-masked-application-data = *( %x00-FF ) ; n*8 bits in length, where n >= 0 frame-unmasked-extension-data = *( %x00-FF ) ; reserved for future extensibility ; n*8 bits in length, where n >= 0 frame-unmasked-application-data = *( %x00-FF ) ; n*8 bits in length, where n >= 0 客户端到服务端的掩码算法

https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3 Client-to-Server Masking

掩码键(Masking-key)是由客户端挑选出来的 32 位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

举例说明:

Octet i of the transformed data ("transformed-octet-i") is the XOR of octet i of the original data ("original-octet-i") with octet at index i modulo 4 of the masking key ("masking-key-octet-j"): j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j original-octet-i:为原始数据的第 i 字节。 transformed-octet-i:为转换后的数据的第 i 字节。 j:为i mod 4的结果。 masking-key-octet-j:为 mask key 第 j 字节。

算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j 数据分片

分片的目的:

有了消息分片,发送一个消息的时候,就可以发送未知大小的信息。如果消息不能被分片,那么就不得不缓冲整个消息,以便计算长度。而有了分片就可以选择合适大小缓冲区来缓冲分片。 第二个目的是可以使用多路复用。

WebSocket 的每条消息(message)可能被切分为多个数据帧。

当 WebSocket 的接收方接收到一个数据帧时,会根据 FIN 值来判断是否收到消息的最后一个数据帧。

从上图可以看出,FIN = 1 时,表示为消息的最后一个数据帧;FIN = 0 时,则不是消息的最后一个数据帧,接收方还要继续监听接收剩余数据帧。

opcode 表示数据传输的类型,0x01 表示文本类型的数据;0x02 表示二进制类型的数据;0x00 比较特殊,表示延续帧(continuation frame),意思就是完整数据对应的数据帧还没有接收完。

更多分片内容请看这里:https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4

消息分片example:

Client: FIN=1, opcode=0x1, msg="hello" Server: (process complete message immediately) Hi. Client: FIN=0, opcode=0x1, msg="and a" Server: (listening, new message containing text started) Client: FIN=0, opcode=0x0, msg="happy new" Server: (listening, payload concatenated to previous message) Client: FIN=1, opcode=0x0, msg="year!" Server: (process complete message) Happy new year to you too!

(具体例子见:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers)

五:怎么保持连接

在第二小结中我们介绍了 websocket 的特点,其中有一个是保持连接状态。

websocket 是建立在 tcp 之上,那也就是客户端与服务端的 tcp 通道要保持连接不断开。

怎么保持呢?可以用心跳来实现。

其实 websocket 协议早就想到了,它的帧数据格式中有一个字段 opcode,定义了 2 种类型操作, ping 和 pong,opcode 分别是 0x9、0xA。

说明:对于长时间没有数据往来的连接,如果依旧长时间保持连接的状态,那么就会浪费连接资源。

[完]

六、参考 https://www.rfc-editor.org/rfc/rfc6455.html WebScoket RFC6455 https://www.rfc-editor.org/rfc/rfc5234 ABNF 格式 https://www.ruanyifeng.com/blog/2017/05/websocket.html websocket 教程,阮一峰 https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有